/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *
 *
 *
 * Created by hattori <hattori@banana-systems.com>
 *                    <hattori@apache.org> (English Only)
 *
 */

#define CORE_PRIVATE
#include "ap_config.h"
#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_protocol.h"
#include "http_log.h"

#include "mod_auth_opensocial.h"

#define strEQ(str1,str2)   (strcmp(str1,str2) == 0)
#define strNEQ(str1,str2)  (strcmp(str1,str2) != 0)

static void *create_auth_opensocial_config_rec(apr_pool_t *p, char *d)
{
    auth_opensocial_config_rec *conf =
        (auth_opensocial_config_rec *) apr_palloc(p, sizeof(auth_opensocial_config_rec));

    conf->enabled    = 0;

    // Both HMAC-SHA1 and RSA-SHA1 is turned off for default.
    conf->hmac_sha1  = 0;
    conf->rsa_sha1   = 0;

    conf->consumer_key            = NULL;
    conf->consumer_secret         = NULL;
    conf->token_secret            = (char *)apr_pstrdup(p, "");
    conf->rsa_publickey_filename  = NULL;
    conf->rsa_publickey           = NULL;

    conf->required_keys           = NULL;
    conf->required_kav            = NULL;

    conf->force_port = 0;
    conf->debug_log_enabled = 0;

    return (void *)conf;
}


static const char *set_accept_signature_methods_slot(cmd_parms *parms, void *mconfig, const char *args)
{
    auth_opensocial_config_rec *conf = mconfig;

    char *auth_types = (char *)apr_pstrdup(parms->temp_pool, args);

    ap_str_tolower(auth_types);

    if(ap_strstr_c(auth_types, "hmac-sha1")){
        conf->hmac_sha1 = 1;
    }else{
        conf->hmac_sha1 = 0;
    }

    if(ap_strstr_c(auth_types, "rsa-sha1")){
        conf->rsa_sha1 = 1;
    }else{
        conf->rsa_sha1 = 0;
    }
 
    return NULL;
}

static const char *set_rsa_publickey_filename_slot(cmd_parms *parms, void *mconfig, const char *arg)
{
    auth_opensocial_config_rec *conf = mconfig; 
    char *rv = (char *)ap_set_file_slot(parms, mconfig, arg);

    if (rv){
        // Invalid file path error !!
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, parms->server, "RSAPublicKeyFile doesn't exist : [%s] %s", arg, rv);
        return rv;
    }
    return (char *)maos_get_file_content(parms, conf->rsa_publickey_filename, &(conf->rsa_publickey)); 
}

static const char *set_force_port_slot(cmd_parms *parms, void *mconfig, const char *arg)
{
    auth_opensocial_config_rec *conf = mconfig;

    int port = atoi(arg);

    if(port < 1 || 65535 < port){
       return "Force Port must be from 1 to 65535";
    }

    conf->force_port = port;

    return NULL;
}

static const char *set_required_key_slot(cmd_parms *parms, void *mconfig, const char *arg)
{

    auth_opensocial_config_rec *conf = mconfig;

    if(!conf->required_keys){
        conf->required_keys = apr_array_make(parms->pool, 1, sizeof(char*));
    }

    *(const char**)apr_array_push(conf->required_keys) = (char *)apr_pstrdup(parms->pool, arg);

    return NULL;
}

static const char *set_required_key_and_value_slot(cmd_parms *parms, void *mconfig, const char *arg1, const char *arg2)
{
    const char *key, *value;
    auth_opensocial_config_rec *conf = mconfig;

    if(!conf->required_kav){
        conf->required_kav = apr_hash_make(parms->pool);
    }
    
    value = apr_hash_get(conf->required_kav, arg1, APR_HASH_KEY_STRING);

    if(value){
        // the key has already been in the hash (conf->required_kav) !!
        return (char *)apr_pstrcat(parms->pool, "AuthOpenSocialRequiredKeyAndValue : Duplicate key error : ", arg1, NULL);
    }else{
        key = (char *)apr_pstrdup(parms->pool, arg1);
        value = (char*)apr_pstrdup(parms->pool, arg2);
        apr_hash_set(conf->required_kav, key, APR_HASH_KEY_STRING, value);
    }

    return NULL;
}

static const command_rec auth_opensocial_cmds[] =
{
    AP_INIT_FLAG("AuthOpenSocialEnabled", ap_set_flag_slot
        , (void *)APR_OFFSETOF(auth_opensocial_config_rec, enabled), OR_LIMIT, ""),
    AP_INIT_TAKE1("AuthOpenSocialRequiredKey", set_required_key_slot
        , NULL, OR_LIMIT, ""),
    AP_INIT_TAKE2("AuthOpenSocialRequiredKeyAndValue", set_required_key_and_value_slot
        , NULL, OR_LIMIT, ""),
    AP_INIT_FLAG("AuthOpenSocialDebugLogEnabled", ap_set_flag_slot
        , (void *)APR_OFFSETOF(auth_opensocial_config_rec, debug_log_enabled), OR_LIMIT, ""),
    AP_INIT_RAW_ARGS("AuthOpenSocialAcceptSignatureMethods", set_accept_signature_methods_slot
        , NULL, OR_LIMIT, ""),
    AP_INIT_TAKE1("AuthOpenSocialConsumerKey", ap_set_string_slot
        , (void *)APR_OFFSETOF(auth_opensocial_config_rec, consumer_key), OR_LIMIT, ""),
    AP_INIT_TAKE1("AuthOpenSocialConsumerSecret", ap_set_string_slot
        , (void *)APR_OFFSETOF(auth_opensocial_config_rec, consumer_secret), OR_LIMIT, ""),
    AP_INIT_TAKE1("AuthOpenSocialRSAPublicKeyFile", set_rsa_publickey_filename_slot
        , (void *)APR_OFFSETOF(auth_opensocial_config_rec, rsa_publickey_filename), OR_LIMIT, ""),
    AP_INIT_TAKE1("AuthOpenSocialForcePort", set_force_port_slot
        , NULL, OR_LIMIT, ""),
    {NULL}
};

static void show_config(auth_opensocial_config_rec *conf)
{
    printf("auth_opensocial_config_rec ---->\n");
    printf("conf: enabled   = %d\n", conf->enabled);
    printf("conf: debug_log_enabled = %d\n", conf->debug_log_enabled);
    printf("conf: hmac_sha1 = %d\n", conf->hmac_sha1);
    printf("conf: rsa_sha1  = %d\n", conf->rsa_sha1);
    printf("conf: consumer_key       = %s\n", conf->consumer_key);
    printf("conf: consumer_secret    = [HIDDEN]\n");
    printf("conf: rsa_publickey_filename = %s\n", conf->rsa_publickey_filename);
    printf("conf: rsa_publickey      = %s\n", conf->rsa_publickey);
    printf("<---- auth_opensocial_config_rec\n");
}

static char *construct_url(apr_pool_t *p, const char *uri,
                                    request_rec *r, apr_port_t force_port)
{

    // This implementation is kinda dirty since ap_get_server_port(...) doesn't 
    // return a correct port in Apache 2.2 unless 
    // UseCanonicalPhysicalPort directive is set to "On".
    //apr_port_t port = ap_get_server_port(r);
    apr_port_t port = r->connection->local_addr->port;

    if(force_port){
        // if force_port is specified, honor its value
        port = force_port;
    }

    const char *host = (char *)ap_get_server_name(r);

#if APR_HAVE_IPV6
    if (ap_strchr_c(host, ':')) { /* IPv6 literal? */
        host = (char *)apr_psprintf(r->pool, "[%s]", host);
    }
#endif

    if (ap_is_default_port(port, r)) {

#if (AP_SERVER_MAJORVERSION_NUMBER == 2 && AP_SERVER_MINORVERSION_NUMBER == 2)
        /* for Apache-2.2 */
        return (char *)apr_pstrcat(p, ap_http_scheme(r), "://", host, uri, NULL);
#elif (defined(AP_SERVER_MAJORVERSION) && defined(AP_SERVER_MINORVERSION))
        return (char *)apr_pstrcat(p, ap_http_method(r), "://", host, uri, NULL);
#endif
    }

#if (AP_SERVER_MAJORVERSION_NUMBER == 2 && AP_SERVER_MINORVERSION_NUMBER == 2)
        /* for Apache-2.2 */
    return (char *)apr_psprintf(p, "%s://%s:%u%s", ap_http_scheme(r), host, port, uri);
#elif (defined(AP_SERVER_MAJORVERSION) && defined(AP_SERVER_MINORVERSION))
    return (char *)apr_psprintf(p, "%s://%s:%u%s", ap_http_method(r), host, port, uri);
#endif
}

static int verify_required_keys(request_rec *r, apr_hash_t *query_params, apr_array_header_t *required_keys)
{
    int i;
    const  char *key ;   
    void *value_in_params;

    if(required_keys == NULL){
        return OK;
    }

    for (i = 0; i < required_keys->nelts; i++) {
        key = ((const char**)required_keys->elts)[i];
        if(!query_params){
            // required keys exist, but params are empty.
            // Obviously, bad request.
            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Missing required key [%s] in the request.", key);
            return HTTP_BAD_REQUEST;
        }else{
            value_in_params = apr_hash_get(query_params, key, APR_HASH_KEY_STRING); 
            if(value_in_params == NULL){
                // Missing required key
                ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Missing required key [%s] in the request.", key);
                return HTTP_BAD_REQUEST;
            }
        }
    }

    return OK;
}

static int verify_required_key_values(request_rec *r, apr_hash_t *query_params, apr_hash_t *required_kav)
{
    int i;
    char *key, *value_in_param, *value, *p;  
    apr_ssize_t klen;  
    apr_hash_index_t *index;  
    apr_array_header_t *values = NULL;

    if(!required_kav){
        return OK;
    }

    for (index = apr_hash_first(r->pool, required_kav);  
            index != NULL;  
            index = apr_hash_next(index)){  

        apr_hash_this(index,  (const void **)&key,  &klen,  (void **)&value);  

        if(!query_params){
            // required key and value exist, but params are empty.
            // Obviously, bad request.
            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Missing required key [%s] and value [%s] in the request.",
                key,
                value);
            return HTTP_BAD_REQUEST;
        }else{

            values = apr_hash_get(query_params, key, APR_HASH_KEY_STRING); 
            if(values){
               for (i = 0; i < values->nelts; i++) {
                   value_in_param = ((char**)values->elts)[i];
                   if(strNEQ(value, value_in_param)){
                       // required value in required_kav and query_params don't match.
                       ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Missing required key [%s] and value [%s] in the request.",
                           key,
                           value);
                       return HTTP_BAD_REQUEST;
                   }
               }
            }else{
               // params don't contain the required key.
               ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Missing required key [%s] and value [%s] in the request.",
                   key,
                   value);
               return HTTP_BAD_REQUEST;
            }
            
        }
    }  

    return OK;
}

static int auth_opensocial_handler(request_rec *r)
{

    int rv = OK;
    apr_hash_t *query_params;
    int num_query_params = 0;
    const char *query_params_in_str;
    char *request_url;
    signed_request_rec *sr; 

    auth_opensocial_config_rec *conf = (auth_opensocial_config_rec *)
        ap_get_module_config(r->per_dir_config, &auth_opensocial_module);

    if ( !conf->enabled ){
        return DECLINED;
    }

    request_url = construct_url(r->pool, r->uri, r, conf->force_port);

    // get all query params and post data from request
    rv = maos_get_http_params(r,&query_params, &num_query_params );

    if(conf->debug_log_enabled){
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Method [%s], URL [%s], PARAMS [%s]",
            r->method, 
            request_url,
            maos_get_query_params_in_str(r, query_params)
        );
    }

    if ( rv != OK ){
        return rv;
    }

    // verify existence of all required keys specified by AuthOpenSocialRequiredKey directive
    rv = verify_required_keys(r, query_params, conf->required_keys);

    if (rv != OK ){
        return rv;
    }

    // verify existence of all pairs of required key and value specified by AuthOpenSocialRequiredKeyAndValue directive
    rv = verify_required_key_values(r, query_params, conf->required_kav);

    if (rv != OK ){
        return rv;
    }

    // request_url is needed to generate a signature base string so hand it to signed_request_rec
    sr = (signed_request_rec *)apr_palloc(r->pool, sizeof(signed_request_rec));
    sr->url = request_url;

    // get oauth params (consumer key...) from query_params and generate a signature base string. 
    // finally, put them into signed_request_rec
    rv = maos_populate_signed_request_rec(r, num_query_params, query_params, sr);

    if (rv != OK){
       return rv;
    }

    // verify signature
    rv = maos_verify_signature(r, sr); 

    if (rv != OK){
        return rv;
    }

    return OK;
}

static int check_sec(apr_array_header_t *sec, char *hostname,  apr_pool_t *ptemp, char **log_msg)
{
    int i;
    int rv = OK;
    auth_opensocial_config_rec *conf;

    for (i = 0; i < sec->nelts; i++) {
        ap_conf_vector_t *vec = ((ap_conf_vector_t **)sec->elts)[i];
        conf = (auth_opensocial_config_rec *)ap_get_module_config(vec,&auth_opensocial_module);
        // core_server_config's sec_url property contains all Location tags (not only this module).
        // Thus, we need null check. if not null, the Location tag contains this module. Otherwise, it doesn't.
        if(conf && conf->enabled){ // checks only when this module is enabled.

            if(conf->hmac_sha1 && ( !conf->consumer_key || !conf->consumer_secret) ){
                // plog should be used instead of ptemp ?
                *log_msg =(char *)apr_pstrcat(ptemp, *log_msg, 
                    hostname,
                    " -> HMAC-SHA1 is turned on but either consumer_key or consumer_secret are not specified. ", NULL);
                rv = !OK;
            }

            if(conf->rsa_sha1 && ( !conf->rsa_publickey_filename || !conf->rsa_publickey) ){
                *log_msg =(char *)apr_pstrcat(ptemp, *log_msg, 
                    hostname,
                    " -> RSA-SHA1 is turned on but publickey file is not specified or is not readable. ", NULL);
                rv = !OK;
            }

        }
    }
    return rv;
}

static int auth_opensocial_post_config(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *sr)
{
    auth_opensocial_config_rec *conf;
    server_rec *current = NULL;
    int i;
    int rv = OK;
    int rv2 = OK;
    char *log_msg = (char *)apr_pstrdup(plog, "");

    current = sr;

    while(current){
		
        core_server_config *core_conf = NULL;
        core_conf = (core_server_config*)ap_get_module_config(current->module_config, &core_module);
    
        rv = check_sec(core_conf->sec_dir, current->server_hostname, plog, &log_msg);
        rv |= check_sec(core_conf->sec_url, current->server_hostname, plog, &log_msg);

        current = current->next;
    }

    if(rv != OK){
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, sr, "mod_auth_opensocial : configuration error : %s", log_msg);
    }

    return rv;
}

static int auth_opensocial_authenticator(request_rec *r)
{
    auth_opensocial_config_rec *conf = (auth_opensocial_config_rec *)
        ap_get_module_config(r->per_dir_config, &auth_opensocial_module);

    if ( !conf->enabled ){
        return DECLINED;
    }

    return OK;
}

static void auth_opensocial_register_hooks(apr_pool_t *p)
{
    ap_hook_post_config(auth_opensocial_post_config, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_check_user_id(auth_opensocial_authenticator, NULL, NULL, APR_HOOK_REALLY_FIRST);
    ap_hook_auth_checker(auth_opensocial_handler, NULL, NULL, APR_HOOK_REALLY_FIRST);
}

/* Dispatch list for API hooks */
module AP_MODULE_DECLARE_DATA auth_opensocial_module = {
    STANDARD20_MODULE_STUFF, 
    create_auth_opensocial_config_rec,  /* create per-dir    config structures */
    NULL,                          /* merge  per-dir    config structures */
    NULL,                          /* create per-server config structures */
    NULL,                          /* merge  per-server config structures */
    auth_opensocial_cmds,          /* table of config file commands       */
    auth_opensocial_register_hooks /* register hooks */
};

